package prod_cons;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;

public class Test {
	
	private static class DummyTask implements Runnable {
		int id;
		long insertionTime = 0;
		public DummyTask(int id) {
			this.id = id;
		}
		public String toString() {
			return Integer.toString(id);
		}
		public void run() {}
	}
	
	private static class Inserter implements Runnable {
		TaskContainer container;
		int peakLength; // num of tasks to insert during the peak
		int timeBetweenPeaks; // milliseconds to wait between peaks (in the average)
		Random random = new Random();
		volatile boolean stop = false;
		int taskId = 0;
			
		LinkedList<Long> timeMeasurements = new LinkedList<Long>();
		
		public Inserter(TaskContainer container, int peakLength, int timeBetweenPeaks) {
			this.container = container;
			this.peakLength = peakLength;
			this.timeBetweenPeaks = timeBetweenPeaks;
		}
		
		public void stop() {
			this.stop = true;
		}
		
		public List<Long> getTimeMeasurements() {
			return this.timeMeasurements;
		}
			
		public void run() {
			while(!stop) {
				// peak period
				long start = System.nanoTime();
				for(int i = 0; i < this.peakLength; i++) {
					DummyTask t = new DummyTask(taskId++);
					if (i%100 == 0) {
						t.insertionTime = System.currentTimeMillis();
					}
					this.container.put(t);
				}
				long finish = System.nanoTime();
				timeMeasurements.push(finish-start);
							
				// silent period
				try {
					long timeToSleep = Math.round(timeBetweenPeaks + Math.sqrt(timeBetweenPeaks) * random.nextGaussian());
					if (timeToSleep > 0)
						Thread.sleep(timeToSleep);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	private static class Remover implements Runnable {
		TaskContainer container;
		volatile boolean stop = false;
		int numOfTasks = 0;
		long maxWaitingTime = 0;
		long waitingMeasurementsSum = 0;
		long waitingMeasurementsCount = 0;
		double throughput = 0;
		
		public Remover(TaskContainer container) {
			this.container = container;
		}
		
		public void stop() {
			this.stop = true;
		}
		
		public int getNumOfTasks() {
			return this.numOfTasks;
		}
		
		public double getThroughput() {
			return this.throughput;
		}
		
		private boolean retrieveTask() {
			Runnable r = container.get();
			if (r != null) {
				if (((DummyTask)r).insertionTime != 0) {
					long waitingTime = System.currentTimeMillis() - ((DummyTask)r).insertionTime;
					if (maxWaitingTime < waitingTime) {
						maxWaitingTime = waitingTime;
					}
					waitingMeasurementsSum += waitingTime;
					waitingMeasurementsCount++;
				}
				numOfTasks++;
			}
			return (r != null); 
		}
		public void run() {
			container.registerConsumer();
			long startTime = System.nanoTime();
			while(!stop) {
				retrieveTask();
			}
			long finishTime = System.nanoTime();
			throughput = ((double)numOfTasks) / (finishTime - startTime) * 1000000; 
			//while(retrieveTask());
		}
	}
	
	private static void printStatistics(Inserter[] inserters, Remover[] removers, int timeToRun, int peakLength) {
		// just print it
		List<Long> measurements = new LinkedList<Long>();
		long numOfInserted = 0;
		long numOfRetrieved = 0;
		long maxWaitingTime = 0;
		long waitingSum = 0;
		long waitingCount = 0;
		double throughputSum = 0;
		double sum = 0;
		for(Inserter inserter : inserters) {
			numOfInserted += inserter.peakLength * inserter.getTimeMeasurements().size();
			for(Long t : inserter.getTimeMeasurements()) {
				measurements.add(t);
				sum += ((double)t)/1000000;
				//System.out.println("Time to insert the tasks: " + ((float)t)/1000000 + " ms");
			}
		}
		double averageInsertion = sum / measurements.size();
		System.out.println("Mean time for insertion = " + averageInsertion + " ms");
		System.out.println("Total num of inserted tasks = " + numOfInserted);
		System.out.println("Peak insertion throughput = " + peakLength * inserters.length * (1/averageInsertion));		
		
		for(Remover remover : removers) {
			numOfRetrieved += remover.numOfTasks;
			if (remover.maxWaitingTime > maxWaitingTime) 
				maxWaitingTime = remover.maxWaitingTime;
			throughputSum += remover.getThroughput();
			waitingSum += remover.waitingMeasurementsSum;
			waitingCount += remover.waitingMeasurementsCount;
		}
		
		System.out.println("Num of retrieved tasks = " + numOfRetrieved + 
							" , system throughput = " + throughputSum);
				//" , system throughput ~ " + ((double)numOfRetrieved)/timeToRun + " tasks/ms");
		System.out.println("Maximal waiting time = " + maxWaitingTime + " ms");
		System.out.println("Average waiting time = " + ((double)waitingSum)/waitingCount + " ms");
		
/*		double[] measurementsArr = new double[measurements.size()];
		int idx = 0;
		double maxVal = 0;
		for(Long m : measurements) {
			double newVal = ((double)m)/1000000;
			measurementsArr[idx++] = newVal;
			if (maxVal < newVal) maxVal = newVal;
		}
		Histogram.drawHistogram(measurementsArr, maxVal);*/
	}
	
	public static void main(String[] args) throws InterruptedException, InstantiationException, IllegalAccessException, ClassNotFoundException {
		int insertersNum = Integer.parseInt(args[0]);
		int removersNum = Integer.parseInt(args[1]);
		int peakLength = Integer.parseInt(args[2]);
		int timeBetweenPeaks = Integer.parseInt(args[3]);
		int timeToRun = Integer.parseInt(args[4]);
		String containerType = args[5];

		TaskContainer container = (TaskContainer)Class.forName(containerType).newInstance();
		container.setConsumersNum(removersNum);
		Inserter[] inserters = new Inserter[insertersNum];
		Thread[] inserterThreads = new Thread[insertersNum];
		for(int i = 0; i < inserters.length; i++) {
			inserters[i] = new Inserter(container, peakLength, timeBetweenPeaks);
			inserterThreads[i] = new Thread(inserters[i]);
		}
		
		Remover[] removers = new Remover[removersNum];
		Thread[] removerThreads = new Thread[removersNum];
		for(int i = 0; i < removers.length; i++) {
			removers[i] = new Remover(container);
			removerThreads[i] = new Thread(removers[i]);
		}
		
		// start the threads
		for(Thread t : inserterThreads) {
			t.start();
		}
		long removersStart = System.nanoTime();
		for(Thread t : removerThreads) {
			t.start();
		}
		
		Thread.sleep(timeToRun);
		
		System.out.println("Stopping the threads...");
		// stop the threads
		for(Inserter inserter : inserters) 
			inserter.stop();
		for(Thread t : inserterThreads) {
			t.join();
		}
		for(Remover remover : removers)
			remover.stop();
				
		for(Thread t : removerThreads) {
			t.join();
		}
		long removersFinish = System.nanoTime();
		
		// print statistics
		printStatistics(inserters, removers, (int)((double)(removersFinish - removersStart)/1000000), peakLength);
	}
}
